feature: events (LAN mini-seasons)#337
Open
Flegma wants to merge 11 commits into
Open
Conversation
is_event_organizer() returns true for the owner, any co-organizer, or
admin-tier roles. Two permission blocks used it where owner-only
semantics were required, letting co-organizers delete events and
add/remove other co-organizers.
- public_events.yaml: delete_permissions for role user now filters on
organizer_steam_id instead of is_organizer.
- public_event_organizers.yaml: insert/delete checks for role user now
require event.organizer_steam_id to match the caller, and an explicit
tournament_organizer block (check/filter: {}) is added so admin-tier
co-organizer management is preserved after narrowing the user role.
…tion get_event_leaderboard was LANGUAGE sql, so its body was parsed at CREATE time against v_player_match_map_hltv, a view applied in a later boot phase. Fresh installs only survived via a fragile session-level check_function_bodies=false. Converted it to LANGUAGE plpgsql, whose body is not parsed for relation references at creation time, and added a RAISE on an unrecognized category to match get_leaderboard's behavior instead of silently returning zeroed rows. The events migration's down.sql dropped the event tables directly, but is_event_organizer (which takes public.events as an argument) and v_event_player_stats (a view over event_tournaments) are created in later boot phases and are not reverted by re-running migrations, so down.sql failed with dependency errors. Prepended drops for get_event_leaderboard, v_event_player_stats and is_event_organizer before the table drops, in dependency order.
…te pushdown The e_matches CTE in v_event_player_stats is referenced 3 times (by kd_agg, assists_agg, matches_agg), so PostgreSQL 12+ materializes it by default. Hasura always queries this view per event, but the materialized CTE builds the match set for every event on the instance before the outer event_id filter discards all but one, so cost scales with total events rather than the target event. Mark the CTE NOT MATERIALIZED so the planner inlines it and pushes the event_id filter down into the event_tournaments scan. This is a planner hint only; verified result-equivalence and EXPLAIN pushdown against the events-verify-db fixtures before committing.
2 tasks
The events table hides Setup-status events from the guest role (filter status _neq Setup), but the child membership tables (event_tournaments/teams/players/organizers), the v_event_player_stats view, and the get_event_leaderboard function all read wide open, so an anonymous client could enumerate a not-yet-public event's roster, tournament list, co-organizers, and computed standings by supplying its id directly. Because events aggregate over pre-existing/finished tournaments, those reads return real populated data before the event goes Live. Gate every event-derived read path the same way the events table does: - guest select on the four child tables and the stats view now filters on event.status _neq Setup. - a user-role select (inherited by every authenticated role) adds the organizer branch so an organizer still sees their own Setup event's children and stats, matching the events table _or [is_organizer, status _neq Setup] pattern. - get_event_leaderboard returns an empty set for a Setup or unknown event instead of computing standings for it.
down.sql dropped the boot-phase view/functions but left their stored digests in migration_hashes.hashes. Because the boot loader skips re-creating an object whose digest is unchanged, a forward deploy after a rollback would recreate the tables but never recreate v_event_player_stats / get_event_leaderboard / is_event_organizer, leaving the events feature silently broken. Delete the three digests in down.sql, guarded by to_regclass so it is a no-op if migration_hashes does not exist yet.
get_event_leaderboard hard-capped its output at LIMIT 100 while the web paginates the tracked function with Hasura-level order_by/limit/offset (the same pattern as the global get_leaderboard, which has no cap), so an event with more than 100 qualifying players would silently truncate both the pages and the aggregate count. Drop the cap. Also coalesce an explicit NULL _min_rounds to 0: HAVING SUM(...) >= NULL filters every row, so a null argument silently emptied the board instead of meaning "no minimum".
The insert checks' role fan-out skipped the moderator tier: no moderator insert permission existed and the match_organizer/tournament_organizer _in lists jumped from streamer to match_organizer. Setting public.create_events_role to 'moderator' would therefore deny event creation to every role below administrator, including moderators themselves. Add the moderator permission block and slot moderator into the higher roles' lists so each list covers every role at or below its own tier. The tournaments clone source (create_tournaments_role checks in public_tournaments.yaml) has the same gap; flagged as a follow-up rather than widened into this PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the approved Events feature: an event is a curated container grouping assigned tournaments; leaderboards and standings are computed on read over the derived match set. Design doc: docs/plans/2026-07-03-events-feature-design.md (workspace docs), implementation plan: docs/plans/2026-07-04-events-feature-implementation-plan.md.
What's included
1867000000300_events: tablesevents,event_organizers,event_tournaments,event_teams,event_players, enume_event_status(Setup/Live/Finished). Event deletion cascades membership rows only; matches/tournaments/players are never touched.is_event_organizercomputed-field function (clone ofis_tournament_organizer).v_event_player_statsview: clone ofv_tournament_player_statskeyed by event, membership derived viaevent_tournaments -> tournament_stages -> tournament_brackets.get_event_leaderboard(_event_id, _category, _match_type, _min_rounds default 10)returningleaderboard_entries; categories rating/adr/kdr/kills/wins; non-emptyevent_playersacts as a roster filter (empty = everyone). Low_min_roundsdefault because the global 50-round floor would blank a one-day LAN.is_organizercomputed field,player_statsmanual relationship, settings-gated insert viapublic.create_events_role(same_existsmechanism as tournaments, full role fan-out), guest select hides Setup events (including all child tables, the stats view, and the leaderboard function), owner-only delete and owner-only co-organizer management, function tracking.Post-review fixes, round 1 (2026-07-04)
An 8-dimension find + adversarial-verify review produced:
status _neq Setupguest gate plus an organizer-awareuserbranch;get_event_leaderboardreturns empty for a Setup or unknown event. Commit5b7a3bf.down.sqlclears the boot-phase SQL digests (guarded byto_regclass) so a forward deploy after a rollback recreates the view/functions. Commitcd8299a.Post-review fixes, round 2 (2026-07-07)
A second full-PR review (8 finder angles, per-candidate adversarial verify, then an adversarial regression pass on the fixes) produced:
get_event_leaderboardhard-capped output atLIMIT 100while the web paginates via Hasura-levelorder_by/limit/offset(like the globalget_leaderboard, which has no cap), silently truncating events with more than 100 players; the cap is removed. An explicit NULL_min_roundsalso silently emptied the board (HAVING >= NULL); it now means "no minimum". Commit1f006a0.create_events_role='moderator'no longer bricks event creation. The insert-check role fan-out skipped the moderator tier entirely (no moderator permission, and the higher roles'_inlists jumped from streamer to match_organizer), so setting the role to moderator would have denied creation to everyone below administrator. Fixed with a moderator permission block and corrected lists. The tournaments clone source (create_tournaments_rolechecks) has the same pre-existing gap; flagged as a follow-up rather than widened into this PR. Commiteed53d6.Verification
Run against an ephemeral TimescaleDB + Hasura v2.49.2 stack with the repo's full migration chain, SQL dirs, metadata, and dev fixtures applied (
metadata ic listconsistent):v_event_player_statsverified row-identical tov_tournament_player_statsfor a single-tournament event (EXCEPT check, both directions), and union growth verified with a second tournament._min_roundsthreshold and_match_typefilter behavior.organizer_steam_id; co-organizers can manage membership but cannot delete the event or manage the co-organizer list (two escalations found and fixed in c77cf4b).Pre-merge checklist (needs the real dev stack)
yarn devboot soHasuraService.setup()hash-applies the new/edited SQL files, thenhasura metadata apply+hasura metadata ic listto confirm no inconsistencies.event_tournaments/v_event_player_stats/get_event_leaderboardreturns zero rows; the organizer still sees them.Product notes for review
get_event_leaderboardtakes no session argument, so it returns empty for a Setup event even to its organizer. This affects the web detail page's Leaderboard tab AND its Teams and Players tab (whose participant fallback uses the same function when no players are explicitly attached); explicitly attached players/teams still show. Raw per-player stats remain visible to the organizer viav_event_player_stats. If organizer preview during Setup is wanted, make the function session-aware (addhasura_session json+is_event_organizercheck) as a follow-up.tournament_organizerrole can manage/delete any event (admin-tier semantics, same asis_event_organizer's role bypass).events.organizer_steam_idFK has no ON DELETE action, so deleting a player who organizes an event is blocked (deliberate; no player-deletion flow exists today).v_event_player_statsderives deaths fromplayer_kills(like its tournament clone) whileget_event_leaderboardusesplayer_match_map_stats, so KDR can differ slightly between the two read paths; the event match-set CTE is duplicated between the view and the function (kept separate to preserve the verified NOT MATERIALIZED inlining); the wins CTE is computed for all categories; no ORDER BY tiebreaker at page boundaries (same as the global leaderboard).Web PR follows (page family + zeus codegen); merge this first.